package edu.amm.neur;

import javafx.event.EventHandler;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;

/**
 * Сетчатая доска для рисования, основанная на канве.
 * 
 * @author Иванов Илья
 * @since 2013-04-25
 *
 */
public class MeshBoard extends Canvas {
	
	private int columns;
	private int rows;
	private int width;
	private int height;
	private int meshStep;
	private int meshLineWidth;
	private Color meshColor;
	private Color fillColor;
	private boolean drawable;
	private Tool tool;
	private boolean[][] matrix;
	private GraphicsContext context = getGraphicsContext2D();
	private EventHandler<MouseEvent> mouseHandler = new EventHandler<MouseEvent>() {
		
		public void handle(MouseEvent event) {
			if (event.getButton() != MouseButton.PRIMARY)
				return;
			
			processMouseAction(event.getX(), event.getY());
		}
	};
	
	/**
	 * Создаёт доску для рисования. По умолчанию инструмент для рисования - карандаш.
	 * 
	 * @param rows Количество квадратов по вертикали.
	 * @param columns Количество квадратов по горизонтали.
	 * @param meshStep Шаг сетки.
	 * @param meshLineWidth Ширина линий сетки.
	 * @param fillColor Цвет зарисованных квадратов.
	 * @param drawable Если <code>true</code>, то на доске можно рисовать.
	 */
	public MeshBoard(int rows, int columns, int meshStep, int meshLineWidth, Color meshColor, Color fillColor, boolean drawable) {
		super(columns * meshStep, rows * meshStep);
		
		if (rows <= 0 || columns <= 0)
			throw new RuntimeException("Некорректные линейные размеры");
		if (meshStep <= 0)
			throw new RuntimeException("Некорректный шаг сетки");
		
		this.rows = rows;
		this.columns = columns;
		height = rows * meshStep;
		width = columns * meshStep;
		this.meshStep = meshStep;
		this.meshLineWidth = meshLineWidth > 0 ? meshLineWidth : DEFAULT_MESH_WIDTH;
		this.meshColor = meshColor == null ? DEFAULT_MESH_COLOR : meshColor;
		this.fillColor = fillColor == null ? DEFAULT_FILL_COLOR : fillColor;
		this.drawable = drawable;
		
		tool = Tool.PENCIL;
		matrix = new boolean[rows][columns];
		
		if (drawable)
			setDrawHandlers();
		drawMesh();
	}
	
	/**
	 * @return Количество строк в изображении.
	 */
	public int getRows() {
		return rows;
	}
	
	/**
	 * @return Количество колонок в изображении.
	 */
	public int getColumns() {
		return columns;
	}
	
	/**
	 * @param tool Инструмент для рисования.
	 */
	public void setTool(Tool tool) {
		boolean stateChanged = (this.tool == null) != (tool == null);
		this.tool = tool;
		if (drawable && stateChanged) {
			if (tool == null)
				removeDrawHandlers();
			else
				setDrawHandlers();
		}
	}
	
	/**
	 * @return Инструмент для рисования.
	 */
	public Tool getTool() {
		return tool;
	}
	
	/**
	 * @return Булевский вектор, соответствующий текущему изображению.
	 */
	public boolean[] getVector() {
		boolean[] vector = new boolean[rows * columns];
		for (int i = 0, k = 0; i < rows; i++)
			for (int j = 0; j < columns; j++, k++)
				vector[k] = matrix[i][j];
		
		return vector;
	}
	
	/**
	 * Отрисовывает изображение, представленное вектором.
	 * @param vector Вектор.
	 * @return <code>true</code>, если изображение отрисовано.
	 */
	public boolean drawVector(boolean[] vector) {
		if (vector == null || vector.length != rows * columns)
			return false;
		
		for (int i = 0, k = 0; i < rows; i++)
			for (int j = 0; j < columns; j++, k++)
				matrix[i][j] = vector[k];
		drawCurrentMatrix();
		
		return true;
	}
	
	/**
	 * Очищает рисунок.
	 */
	public void clear() {
		for (int i = 0; i < rows; i++)
			for (int j = 0; j < columns; j++)
				matrix[i][j] = false;
		
		drawCurrentMatrix();
	}
	
	private void setDrawHandlers() {
		addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
		addEventHandler(MouseEvent.MOUSE_DRAGGED, mouseHandler);
	}
	
	private void removeDrawHandlers() {
		removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
		removeEventHandler(MouseEvent.MOUSE_DRAGGED, mouseHandler);
	}
	
	private void drawMesh() {
		context.save();
		
		context.setStroke(meshColor);
		context.setLineWidth(meshLineWidth);
		
		// Горизонтальные линии
		for (int y = 0; y <= height; y += meshStep) {
			context.beginPath();
			context.moveTo(0, y);
			context.lineTo(width, y);
			context.stroke();
		}
		
		// Вертикальные линии
		for (int x = 0; x <= width; x += meshStep) {
			context.beginPath();
			context.moveTo(x, 0);
			context.lineTo(x, height);
			context.stroke();
		}
		
		context.restore();
	}
	
	private void processMouseAction(double x, double y) {
		if (x < 0 || x >= width || y < 0 || y >= height)
			return;
		
		processSquare(new LogicalCoordinates(x, y));
	}
	
	private void processSquare(LogicalCoordinates log) {
		switch (tool) {
			case PENCIL:
				if (matrix[log.getI()][log.getJ()])
					return;
				
				matrix[log.getI()][log.getJ()] = true;
				fillSquare(new PhysicalCoordinates(log));
				break;
			case ERASER:
				if (!matrix[log.getI()][log.getJ()])
					return;
				
				matrix[log.getI()][log.getJ()] = false;
				drawCurrentMatrix();
				break;
		}
	}
	
	private void fillSquare(PhysicalCoordinates phys) {
		context.save();
		
		context.setFill(fillColor);
		context.fillRect(phys.getX(), phys.getY(), meshStep, meshStep);
		
		context.restore();
	}
	
	private void drawCurrentMatrix() {
		context.clearRect(0, 0, width, height);
		drawMesh();
		for (int i = 0; i < rows; i++)
			for (int j = 0; j < columns; j++)
				if (matrix[i][j])
					fillSquare(new PhysicalCoordinates(i, j));
	}
	
	private static final int DEFAULT_MESH_WIDTH = 1;
	private static final Color DEFAULT_MESH_COLOR = Color.GRAY;
	private static final Color DEFAULT_FILL_COLOR = Color.BLACK;
	
	private class LogicalCoordinates {
		
		private int i;
		private int j;
		
		public LogicalCoordinates(double x, double y) {
			i = (int) y / meshStep;
			j = (int) x / meshStep;
		}
		
		public int getI() {
			return i;
		}
		
		public int getJ() {
			return j;
		}
	}
	
	private class PhysicalCoordinates {
		
		private double x;
		private double y;
		
		public PhysicalCoordinates(int i, int j) {
			init(i, j);
		}
		
		public PhysicalCoordinates(LogicalCoordinates log) {
			if (log == null)
				throw new RuntimeException();
			
			init(log.getI(), log.getJ());
		}
		
		private void init(int i, int j) {
			x = j * meshStep;
			y = i * meshStep;
		}
		
		public double getX() {
			return x;
		}
		
		public double getY() {
			return y;
		}
	}
	
	public static enum Tool {
		
		PENCIL,
		ERASER
	}
}